Implement a package id specification format
authorAlex Crichton <alex@alexcrichton.com>
Mon, 22 Sep 2014 00:11:36 +0000 (17:11 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Mon, 22 Sep 2014 22:41:18 +0000 (15:41 -0700)
src/cargo/core/mod.rs
src/cargo/core/package_id.rs
src/cargo/core/package_id_spec.rs [new file with mode: 0644]
src/cargo/util/mod.rs
src/cargo/util/to_semver.rs [new file with mode: 0644]
src/cargo/util/toml.rs

index 48365629f75d5a3e74c547fe8bc94e3a95f3156e..50d86caa4d7cc4747b6fabf68d18bee6f6d5b635 100644 (file)
@@ -1,48 +1,14 @@
-pub use self::registry::{
-    Registry,
-};
-
-pub use self::manifest::{
-    Manifest,
-    Target,
-    TargetKind,
-    Profile
-};
-
-pub use self::package::{
-    Package,
-    PackageSet
-};
-
-pub use self::package_id::{
-    PackageId
-};
-
-pub use self::source::{
-    Source,
-    SourceId,
-    SourceMap,
-    SourceSet,
-    GitKind,
-    PathKind,
-    RegistryKind
-};
-
-pub use self::summary::{
-    Summary
-};
-
-pub use self::shell::{
-    Shell,
-    MultiShell,
-    ShellConfig
-};
-
-pub use self::dependency::{
-    Dependency
-};
-
+pub use self::dependency::Dependency;
+pub use self::manifest::{Manifest, Target, TargetKind, Profile};
+pub use self::package::{Package, PackageSet};
+pub use self::package_id::PackageId;
+pub use self::package_id_spec::PackageIdSpec;
+pub use self::registry::Registry;
 pub use self::resolver::Resolve;
+pub use self::shell::{Shell, MultiShell, ShellConfig};
+pub use self::source::{PathKind, RegistryKind};
+pub use self::source::{Source, SourceId, SourceMap, SourceSet, GitKind};
+pub use self::summary::Summary;
 
 pub mod source;
 pub mod package;
@@ -53,3 +19,4 @@ pub mod resolver;
 pub mod summary;
 pub mod shell;
 pub mod registry;
+mod package_id_spec;
index 49ee9abc04ea9a407a3209e37bbb02d66f01cbc8..810456c74776e740ef2cc4c808aab3382627f612 100644 (file)
@@ -2,34 +2,10 @@ use semver;
 use std::hash::Hash;
 use std::fmt::{mod, Show, Formatter};
 use std::hash;
-use serialize::{
-    Encodable,
-    Encoder,
-    Decodable,
-    Decoder
-};
-
-use util::{CargoResult, CargoError, short_hash};
-use core::source::SourceId;
-
-trait ToVersion {
-    fn to_version(self) -> Result<semver::Version, String>;
-}
-
-impl ToVersion for semver::Version {
-    fn to_version(self) -> Result<semver::Version, String> {
-        Ok(self)
-    }
-}
+use serialize::{Encodable, Encoder, Decodable, Decoder};
 
-impl<'a> ToVersion for &'a str {
-    fn to_version(self) -> Result<semver::Version, String> {
-        match semver::Version::parse(self) {
-            Ok(v) => Ok(v),
-            Err(_) => Err(format!("cannot parse '{}' as a semver", self)),
-        }
-    }
-}
+use util::{CargoResult, CargoError, short_hash, ToSemver};
+use core::source::SourceId;
 
 #[deriving(Clone, PartialEq, PartialOrd, Ord)]
 pub struct PackageId {
@@ -97,9 +73,9 @@ pub struct Metadata {
 }
 
 impl PackageId {
-    pub fn new<T: ToVersion>(name: &str, version: T,
+    pub fn new<T: ToSemver>(name: &str, version: T,
                              sid: &SourceId) -> CargoResult<PackageId> {
-        let v = try!(version.to_version().map_err(InvalidVersion));
+        let v = try!(version.to_semver().map_err(InvalidVersion));
         Ok(PackageId {
             name: name.to_string(),
             version: v,
diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs
new file mode 100644 (file)
index 0000000..bfce8cd
--- /dev/null
@@ -0,0 +1,235 @@
+use std::fmt;
+use semver::Version;
+use url::{mod, Url, UrlParser};
+
+use core::PackageId;
+use util::{CargoResult, ToUrl, Require, human, ToSemver};
+
+#[deriving(Clone, PartialEq, Eq)]
+pub struct PackageIdSpec {
+    name: String,
+    version: Option<Version>,
+    url: Option<Url>,
+}
+
+impl PackageIdSpec {
+    pub fn parse(spec: &str) -> CargoResult<PackageIdSpec> {
+        if spec.contains("/") {
+            match spec.to_url() {
+                Ok(url) => return PackageIdSpec::from_url(url),
+                Err(..) => {}
+            }
+            if !spec.contains("://") {
+                match url(format!("cargo://{}", spec).as_slice()) {
+                    Ok(url) => return PackageIdSpec::from_url(url),
+                    Err(..) => {}
+                }
+            }
+        }
+        let mut parts = spec.as_slice().splitn(1, ':');
+        let name = parts.next().unwrap();
+        let version = match parts.next() {
+            Some(version) => Some(try!(Version::parse(version).map_err(human))),
+            None => None,
+        };
+        for ch in name.chars() {
+            if !ch.is_alphanumeric() && ch != '_' && ch != '-' {
+                return Err(human(format!("invalid character in pkgid `{}`: `{}`",
+                                         spec, ch)))
+            }
+        }
+        Ok(PackageIdSpec {
+            name: name.to_string(),
+            version: version,
+            url: None,
+        })
+    }
+
+    pub fn from_package_id(package_id: &PackageId) -> PackageIdSpec {
+        PackageIdSpec {
+            name: package_id.get_name().to_string(),
+            version: Some(package_id.get_version().clone()),
+            url: Some(package_id.get_source_id().url.clone()),
+        }
+    }
+
+    fn from_url(mut url: Url) -> CargoResult<PackageIdSpec> {
+        if url.query.is_some() {
+            return Err(human(format!("cannot have a query string in a pkgid: {}",
+                             url)));
+        }
+        let frag = url.fragment.take();
+        let (name, version) = {
+            let path = try!(url.path().require(|| {
+                human(format!("pkgid urls must have a path: {}", url))
+            }));
+            let path_name = try!(path.last().require(|| {
+                human(format!("pkgid urls must have at least one path \
+                               component: {}", url))
+            }));
+            match frag {
+                Some(fragment) => {
+                    let mut parts = fragment.as_slice().splitn(1, ':');
+                    let name_or_version = parts.next().unwrap();
+                    match parts.next() {
+                        Some(part) => {
+                            let version = try!(part.to_semver().map_err(human));
+                            (name_or_version.to_string(), Some(version))
+                        }
+                        None => {
+                            if name_or_version.char_at(0).is_alphabetic() {
+                                (name_or_version.to_string(), None)
+                            } else {
+                                let version = try!(name_or_version.to_semver()
+                                                                  .map_err(human));
+                                (path_name.to_string(), Some(version))
+                            }
+                        }
+                    }
+                }
+                None => (path_name.to_string(), None),
+            }
+        };
+        Ok(PackageIdSpec {
+            name: name,
+            version: version,
+            url: Some(url),
+        })
+    }
+
+    pub fn get_name(&self) -> &str { self.name.as_slice() }
+    pub fn get_version(&self) -> Option<&Version> { self.version.as_ref() }
+    pub fn get_url(&self) -> Option<&Url> { self.url.as_ref() }
+
+    pub fn matches(&self, package_id: &PackageId) -> bool {
+        if self.get_name() != package_id.get_name() { return false }
+
+        match self.version {
+            Some(ref v) => if v != package_id.get_version() { return false },
+            None => {}
+        }
+
+        match self.url {
+            Some(ref u) => *u == package_id.get_source_id().url,
+            None => true
+        }
+    }
+}
+
+fn url(s: &str) -> url::ParseResult<Url> {
+    return UrlParser::new().scheme_type_mapper(mapper).parse(s);
+
+    fn mapper(scheme: &str) -> url::SchemeType {
+        if scheme == "cargo" {
+            url::RelativeScheme(1)
+        } else {
+            url::whatwg_scheme_type_mapper(scheme)
+        }
+    }
+
+}
+
+impl fmt::Show for PackageIdSpec {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut printed_name = false;
+        match self.url {
+            Some(ref url) => {
+                if url.scheme.as_slice() == "cargo" {
+                    try!(write!(f, "{}/{}", url.host().unwrap(),
+                                url.path().unwrap().connect("/")));
+                } else {
+                    try!(write!(f, "{}", url));
+                }
+                if url.path().unwrap().last().unwrap() != &self.name {
+                    printed_name = true;
+                    try!(write!(f, "#{}", self.name));
+                }
+            }
+            None => { printed_name = true; try!(write!(f, "{}", self.name)) }
+        }
+        match self.version {
+            Some(ref v) => {
+                try!(write!(f, "{}{}", if printed_name {":"} else {"#"}, v));
+            }
+            None => {}
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::{PackageId, SourceId};
+    use super::{PackageIdSpec, url};
+    use semver::Version;
+
+    #[test]
+    fn good_parsing() {
+        fn ok(spec: &str, expected: PackageIdSpec) {
+            let parsed = PackageIdSpec::parse(spec).unwrap();
+            assert_eq!(parsed, expected);
+            assert_eq!(parsed.to_string().as_slice(), spec);
+        }
+
+        ok("http://crates.io/foo#1.2.3", PackageIdSpec {
+            name: "foo".to_string(),
+            version: Some(Version::parse("1.2.3").unwrap()),
+            url: Some(url("http://crates.io/foo").unwrap()),
+        });
+        ok("http://crates.io/foo#bar:1.2.3", PackageIdSpec {
+            name: "bar".to_string(),
+            version: Some(Version::parse("1.2.3").unwrap()),
+            url: Some(url("http://crates.io/foo").unwrap()),
+        });
+        ok("crates.io/foo", PackageIdSpec {
+            name: "foo".to_string(),
+            version: None,
+            url: Some(url("cargo://crates.io/foo").unwrap()),
+        });
+        ok("crates.io/foo#1.2.3", PackageIdSpec {
+            name: "foo".to_string(),
+            version: Some(Version::parse("1.2.3").unwrap()),
+            url: Some(url("cargo://crates.io/foo").unwrap()),
+        });
+        ok("crates.io/foo#bar", PackageIdSpec {
+            name: "bar".to_string(),
+            version: None,
+            url: Some(url("cargo://crates.io/foo").unwrap()),
+        });
+        ok("crates.io/foo#bar:1.2.3", PackageIdSpec {
+            name: "bar".to_string(),
+            version: Some(Version::parse("1.2.3").unwrap()),
+            url: Some(url("cargo://crates.io/foo").unwrap()),
+        });
+        ok("foo", PackageIdSpec {
+            name: "foo".to_string(),
+            version: None,
+            url: None,
+        });
+        ok("foo:1.2.3", PackageIdSpec {
+            name: "foo".to_string(),
+            version: Some(Version::parse("1.2.3").unwrap()),
+            url: None,
+        });
+    }
+
+    #[test]
+    fn bad_parsing() {
+        assert!(PackageIdSpec::parse("baz:").is_err());
+        assert!(PackageIdSpec::parse("baz:1.0").is_err());
+        assert!(PackageIdSpec::parse("http://baz:1.0").is_err());
+        assert!(PackageIdSpec::parse("http://#baz:1.0").is_err());
+    }
+
+    #[test]
+    fn matching() {
+        let sid = SourceId::for_central().unwrap();
+        let foo = PackageId::new("foo", "1.2.3", &sid).unwrap();
+        let bar = PackageId::new("bar", "1.2.3", &sid).unwrap();
+
+        assert!( PackageIdSpec::parse("foo").unwrap().matches(&foo));
+        assert!(!PackageIdSpec::parse("foo").unwrap().matches(&bar));
+        assert!( PackageIdSpec::parse("foo:1.2.3").unwrap().matches(&foo));
+        assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(&foo));
+    }
+}
index 38614d307b6920ac7114d0ee24bf91fe2536292a..6e14c33856debfd8ada7b21af14ecf213141c86b 100644 (file)
@@ -11,6 +11,7 @@ pub use self::dependency_queue::{DependencyQueue, Fresh, Dirty, Freshness};
 pub use self::dependency_queue::Dependency;
 pub use self::graph::Graph;
 pub use self::to_url::ToUrl;
+pub use self::to_semver::ToSemver;
 pub use self::vcs::{GitRepo, HgRepo};
 pub use self::sha256::Sha256;
 
@@ -24,6 +25,7 @@ pub mod paths;
 pub mod errors;
 pub mod hex;
 pub mod profile;
+pub mod to_semver;
 mod pool;
 mod dependency_queue;
 mod to_url;
diff --git a/src/cargo/util/to_semver.rs b/src/cargo/util/to_semver.rs
new file mode 100644 (file)
index 0000000..9ea9216
--- /dev/null
@@ -0,0 +1,18 @@
+use semver::Version;
+
+pub trait ToSemver {
+    fn to_semver(self) -> Result<Version, String>;
+}
+
+impl ToSemver for Version {
+    fn to_semver(self) -> Result<Version, String> { Ok(self) }
+}
+
+impl<'a> ToSemver for &'a str {
+    fn to_semver(self) -> Result<Version, String> {
+        match Version::parse(self) {
+            Ok(v) => Ok(v),
+            Err(..) => Err(format!("cannot parse '{}' as a semver", self)),
+        }
+    }
+}
index 0d8deaf52a8681ec92144455241782c4a745d6c6..1e417f5496cbb0ad1b035a1e03aa85f54c086a9a 100644 (file)
@@ -13,7 +13,7 @@ use core::{SourceId, GitKind};
 use core::manifest::{LibKind, Lib, Dylib, Profile};
 use core::{Summary, Manifest, Target, Dependency, PackageId};
 use core::package_id::Metadata;
-use util::{CargoResult, Require, human, ToUrl};
+use util::{CargoResult, Require, human, ToUrl, ToSemver};
 
 /// Representation of the projects file layout.
 ///
@@ -262,10 +262,9 @@ pub struct TomlVersion {
 impl<E, D: Decoder<E>> Decodable<D, E> for TomlVersion {
     fn decode(d: &mut D) -> Result<TomlVersion, E> {
         let s = raw_try!(d.read_str());
-        match semver::Version::parse(s.as_slice()) {
+        match s.as_slice().to_semver() {
             Ok(s) => Ok(TomlVersion { version: s }),
-            Err(_) => Err(d.error(format!("cannot parse '{}' as a semver",
-                                        s).as_slice())),
+            Err(e) => Err(d.error(e.as_slice())),
         }
     }
 }